This guide walks you through building a Chrome extension that uses a **persistent two-way communication channel** between a **popup script** and the **background service worker** using `chrome.runtime.connect()`.
We will create a chrome extension that has this popup interface:
![[Pasted image 20250407011640.png]]
You can't send messages unless you make a two-way connection ("Connect" button). You can disconnect any time. When you sent a message, this is how it looks:
![[Pasted image 20250407011731.png]]
---
### π¦ File Structure
```
.
βββ background.js
βββ icon.png
βββ icon128x128.png
βββ icon16x16.png
βββ icon32x32.png
βββ icon48x48.png
βββ manifest.json
βββ popup.css
βββ popup.html
βββ popup.js
```
---
### π§ How It Works
We use `chrome.runtime.connect()` to create a **persistent port** between the popup and background script. This allows real-time, bidirectional communication while the popup is open.
---
## πͺ Breakdown by File
---
### π `manifest.json`
Declares the extension's entry points and permissions.
```json
{
"name": "Popup to Background Messaging",
"manifest_version": 3,
"version": "1.0",
"description": "Example extension using ports between popup and background.",
"icons": {
"16": "icon16x16.png",
"32": "icon32x32.png",
"48": "icon48x48.png",
"128": "icon128x128.png"
},
"content_security_policy": {
"extension_pages": "default-src 'self'; script-src 'self'; object-src 'self'; style-src 'self'; img-src 'self' *; connect-src 'self'"
},
"permissions": [
"activeTab",
"tabs"
],
"host_permissions": [],
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"background": {
"service_worker": "background.js"
}
}
```
---
### π§ `background.js`
Handles incoming connections and routes messages from the popup.
```js
let popupPort = null;
chrome.runtime.onConnect.addListener(port => {
if (port.name === "popup-connection") {
popupPort = port;
port.onMessage.addListener(msg => {
console.log("Message from popup:", msg);
// Respond to popup
popupPort.postMessage({ reply: `Background received: "${msg.fromPopup}"` });
});
port.onDisconnect.addListener(() => {
console.log("Popup disconnected");
popupPort = null;
});
}
});
```
**Key Points:**
- Listens for connections via `onConnect`.
- Handles messages with `port.onMessage.addListener`.
- Responds back using `port.postMessage`.
---
### π `popup.html`
The user interface for sending and receiving messages.
```html
Popup Messaging
Message Background
Response will appear here
```
---
### π¨ `popup.css`
Basic styling for the popup interface.
```css
body {
font-family: sans-serif;
padding: 10px;
width: 250px;
}
button,
input {
margin-top: 5px;
padding: 5px;
}
#response {
margin-top: 10px;
font-weight: bold;
}
```
---
### βοΈ `popup.js`
Controls the UI, manages connection state, and handles messaging.
```js
let port = null;
const connectBtn = document.getElementById("connectBtn");
const disconnectBtn = document.getElementById("disconnectBtn");
const input = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
const responseDiv = document.getElementById("response");
function updateUI(connected) {
input.disabled = !connected;
sendBtn.disabled = !connected;
connectBtn.disabled = connected;
disconnectBtn.disabled = !connected;
}
connectBtn.addEventListener("click", () => {
port = chrome.runtime.connect({ name: "popup-connection" });
updateUI(true);
port.onMessage.addListener(msg => {
console.log("Received from background:", msg);
responseDiv.textContent = `Reply: ${msg.reply || JSON.stringify(msg)}`;
});
port.onDisconnect.addListener(() => {
console.log("Port disconnected");
port = null;
updateUI(false);
});
});
disconnectBtn.addEventListener("click", () => {
if (port) {
port.disconnect();
port = null;
updateUI(false);
}
});
sendBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!port) {
alert("Not connected to background script!");
return;
}
if (text) {
port.postMessage({ fromPopup: text });
}
});
```
**Key Points:**
- Connects to background via `chrome.runtime.connect`.
- UI updates based on connection state.
- Sends messages through the port.
- Displays background responses in real time.
---
## β How to Test
1. **Load the extension**:
- Visit `chrome://extensions`
- Enable "Developer mode"
- Click **Load unpacked** and select your extension folder.
2. **Click the extension icon** to open the popup.
3. **Click "Connect"**, type a message, and click "Send".
4. **Watch console logs** in:
- Popup (`Right-click > Inspect`)
- Background (`chrome://extensions > Service Worker > Inspect`)
----
## β **Yes, this _could_ be done with `chrome.runtime.sendMessage` + `sendResponse`**
But thatβs really best for **one-off, request/response** interactions.
Example:
```js
// Popup
chrome.runtime.sendMessage({ greeting: "hi" }, response => {
console.log("Got reply:", response);
});
// Background
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.greeting === "hi") {
sendResponse({ reply: "hey back!" });
}
});
```
> π¬ This is **fire-and-forget with a single reply**. It doesnβt keep a persistent connection.
---
### π Why `chrome.runtime.connect()` is Better for Two-Way Conversations
Using `connect()` gives you a **persistent message port**, which is ideal for:
- Constant back-and-forth messaging (like chat, logs, status updates)
- Two panels staying in sync (like popup + sidebar or devtools + content script)
- Full-duplex streams, even with queued messages
- Better control over connect/disconnect events
---
### β Your Setup is Perfect for...
- A **sidebar UI** and a **popup** talking to each other through the background
- Streaming data back and forth
- Reacting to UI changes across contexts
- Chat-like features or coordination across extension views
If youβre thinking of hooking this up to a **sidebar**, `devtools`, or even a **content script**, this structure is already halfway there. This tutorial proves there is an "active" connection because you click the "Connect" button and you can disconnect by clicking the "Disconnect" button any time, but messages can only be sent when there's an active connection.